3 * Provide things related to namespaces.
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
23 use MediaWiki\Config\ServiceOptions
;
26 * This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of
27 * them based on index. The textual names of the namespaces are handled by Language.php.
34 * These namespaces should always be first-letter capitalized, now and
35 * forevermore. Historically, they could've probably been lowercased too,
36 * but some things are just too ingrained now. :)
38 private $alwaysCapitalizedNamespaces = [ NS_SPECIAL
, NS_USER
, NS_MEDIAWIKI
];
40 /** @var string[]|null Canonical namespaces cache */
41 private $canonicalNamespaces = null;
43 /** @var array|false Canonical namespaces index cache */
44 private $namespaceIndexes = false;
46 /** @var int[]|null Valid namespaces cache */
47 private $validNamespaces = null;
49 /** @var ServiceOptions */
53 * TODO Make this const when HHVM support is dropped (T192166)
58 public static $constructorOptions = [
60 'CanonicalNamespaceNames',
61 'CapitalLinkOverrides',
65 'ExtraSignatureNamespaces',
66 'NamespaceContentModels',
67 'NamespaceProtection',
68 'NamespacesWithSubpages',
69 'NonincludableNamespaces',
74 * @param ServiceOptions $options
76 public function __construct( ServiceOptions
$options ) {
77 $options->assertRequiredOptions( self
::$constructorOptions );
78 $this->options
= $options;
82 * Throw an exception when trying to get the subject or talk page
83 * for a given namespace where it does not make sense.
84 * Special namespaces are defined in includes/Defines.php and have
85 * a value below 0 (ex: NS_SPECIAL = -1 , NS_MEDIA = -2)
88 * @param string $method
93 private function isMethodValidFor( $index, $method ) {
94 if ( $index < NS_MAIN
) {
95 throw new MWException( "$method does not make any sense for given namespace $index" );
101 * Can pages in the given namespace be moved?
103 * @param int $index Namespace index
106 public function isMovable( $index ) {
107 $result = $index >= NS_MAIN
&&
108 ( $index != NS_FILE ||
$this->options
->get( 'AllowImageMoving' ) );
113 Hooks
::run( 'NamespaceIsMovable', [ $index, &$result ] );
119 * Is the given namespace is a subject (non-talk) namespace?
121 * @param int $index Namespace index
124 public function isSubject( $index ) {
125 return !$this->isTalk( $index );
129 * Is the given namespace a talk namespace?
131 * @param int $index Namespace index
134 public function isTalk( $index ) {
135 return $index > NS_MAIN
140 * Get the talk namespace index for a given namespace
142 * @param int $index Namespace index
145 public function getTalk( $index ) {
146 $this->isMethodValidFor( $index, __METHOD__
);
147 return $this->isTalk( $index )
153 * Get the subject namespace index for a given namespace
154 * Special namespaces (NS_MEDIA, NS_SPECIAL) are always the subject.
156 * @param int $index Namespace index
159 public function getSubject( $index ) {
160 # Handle special namespaces
161 if ( $index < NS_MAIN
) {
165 return $this->isTalk( $index )
171 * Get the associated namespace.
172 * For talk namespaces, returns the subject (non-talk) namespace
173 * For subject (non-talk) namespaces, returns the talk namespace
175 * @param int $index Namespace index
178 public function getAssociated( $index ) {
179 $this->isMethodValidFor( $index, __METHOD__
);
181 if ( $this->isSubject( $index ) ) {
182 return $this->getTalk( $index );
184 return $this->getSubject( $index );
188 * Returns whether the specified namespace exists
194 public function exists( $index ) {
195 $nslist = $this->getCanonicalNamespaces();
196 return isset( $nslist[$index] );
200 * Returns whether the specified namespaces are the same namespace
202 * @note It's possible that in the future we may start using something
203 * other than just namespace indexes. Under that circumstance making use
204 * of this function rather than directly doing comparison will make
205 * sure that code will not potentially break.
207 * @param int $ns1 The first namespace index
208 * @param int $ns2 The second namespace index
212 public function equals( $ns1, $ns2 ) {
217 * Returns whether the specified namespaces share the same subject.
218 * eg: NS_USER and NS_USER wil return true, as well
219 * NS_USER and NS_USER_TALK will return true.
221 * @param int $ns1 The first namespace index
222 * @param int $ns2 The second namespace index
226 public function subjectEquals( $ns1, $ns2 ) {
227 return $this->getSubject( $ns1 ) == $this->getSubject( $ns2 );
231 * Returns array of all defined namespaces with their canonical
236 public function getCanonicalNamespaces() {
237 if ( $this->canonicalNamespaces
=== null ) {
238 $this->canonicalNamespaces
=
239 [ NS_MAIN
=> '' ] +
$this->options
->get( 'CanonicalNamespaceNames' );
240 $this->canonicalNamespaces +
=
241 ExtensionRegistry
::getInstance()->getAttribute( 'ExtensionNamespaces' );
242 if ( is_array( $this->options
->get( 'ExtraNamespaces' ) ) ) {
243 $this->canonicalNamespaces +
= $this->options
->get( 'ExtraNamespaces' );
245 Hooks
::run( 'CanonicalNamespaces', [ &$this->canonicalNamespaces
] );
247 return $this->canonicalNamespaces
;
251 * Returns the canonical (English) name for a given index
253 * @param int $index Namespace index
254 * @return string|bool If no canonical definition.
256 public function getCanonicalName( $index ) {
257 $nslist = $this->getCanonicalNamespaces();
258 return $nslist[$index] ??
false;
262 * Returns the index for a given canonical name, or NULL
263 * The input *must* be converted to lower case first
265 * @param string $name Namespace name
268 public function getCanonicalIndex( $name ) {
269 if ( $this->namespaceIndexes
=== false ) {
270 $this->namespaceIndexes
= [];
271 foreach ( $this->getCanonicalNamespaces() as $i => $text ) {
272 $this->namespaceIndexes
[strtolower( $text )] = $i;
275 if ( array_key_exists( $name, $this->namespaceIndexes
) ) {
276 return $this->namespaceIndexes
[$name];
283 * Returns an array of the namespaces (by integer id) that exist on the wiki. Used primarily by
284 * the API in help documentation. The array is sorted numerically and omits negative namespaces.
287 public function getValidNamespaces() {
288 if ( is_null( $this->validNamespaces
) ) {
289 foreach ( array_keys( $this->getCanonicalNamespaces() ) as $ns ) {
291 $this->validNamespaces
[] = $ns;
294 // T109137: sort numerically
295 sort( $this->validNamespaces
, SORT_NUMERIC
);
298 return $this->validNamespaces
;
304 * Does this namespace ever have a talk namespace?
306 * @param int $index Namespace ID
307 * @return bool True if this namespace either is or has a corresponding talk namespace.
309 public function hasTalkNamespace( $index ) {
310 return $index >= NS_MAIN
;
314 * Does this namespace contain content, for the purposes of calculating
317 * @param int $index Index to check
320 public function isContent( $index ) {
321 return $index == NS_MAIN ||
in_array( $index, $this->options
->get( 'ContentNamespaces' ) );
325 * Might pages in this namespace require the use of the Signature button on
328 * @param int $index Index to check
331 public function wantSignatures( $index ) {
332 return $this->isTalk( $index ) ||
333 in_array( $index, $this->options
->get( 'ExtraSignatureNamespaces' ) );
337 * Can pages in a namespace be watched?
342 public function isWatchable( $index ) {
343 return $index >= NS_MAIN
;
347 * Does the namespace allow subpages?
349 * @param int $index Index to check
352 public function hasSubpages( $index ) {
353 return !empty( $this->options
->get( 'NamespacesWithSubpages' )[$index] );
357 * Get a list of all namespace indices which are considered to contain content
358 * @return array Array of namespace indices
360 public function getContentNamespaces() {
361 $contentNamespaces = $this->options
->get( 'ContentNamespaces' );
362 if ( !is_array( $contentNamespaces ) ||
$contentNamespaces === [] ) {
364 } elseif ( !in_array( NS_MAIN
, $contentNamespaces ) ) {
365 // always force NS_MAIN to be part of array (to match the algorithm used by isContent)
366 return array_merge( [ NS_MAIN
], $contentNamespaces );
368 return $contentNamespaces;
373 * List all namespace indices which are considered subject, aka not a talk
374 * or special namespace. See also NamespaceInfo::isSubject
376 * @return array Array of namespace indices
378 public function getSubjectNamespaces() {
380 $this->getValidNamespaces(),
381 [ $this, 'isSubject' ]
386 * List all namespace indices which are considered talks, aka not a subject
387 * or special namespace. See also NamespaceInfo::isTalk
389 * @return array Array of namespace indices
391 public function getTalkNamespaces() {
393 $this->getValidNamespaces(),
399 * Is the namespace first-letter capitalized?
401 * @param int $index Index to check
404 public function isCapitalized( $index ) {
405 // Turn NS_MEDIA into NS_FILE
406 $index = $index === NS_MEDIA ? NS_FILE
: $index;
408 // Make sure to get the subject of our namespace
409 $index = $this->getSubject( $index );
411 // Some namespaces are special and should always be upper case
412 if ( in_array( $index, $this->alwaysCapitalizedNamespaces
) ) {
415 $overrides = $this->options
->get( 'CapitalLinkOverrides' );
416 if ( isset( $overrides[$index] ) ) {
417 // CapitalLinkOverrides is explicitly set
418 return $overrides[$index];
420 // Default to the global setting
421 return $this->options
->get( 'CapitalLinks' );
425 * Does the namespace (potentially) have different aliases for different
426 * genders. Not all languages make a distinction here.
428 * @param int $index Index to check
431 public function hasGenderDistinction( $index ) {
432 return $index == NS_USER ||
$index == NS_USER_TALK
;
436 * It is not possible to use pages from this namespace as template?
438 * @param int $index Index to check
441 public function isNonincludable( $index ) {
442 $namespaces = $this->options
->get( 'NonincludableNamespaces' );
443 return $namespaces && in_array( $index, $namespaces );
447 * Get the default content model for a namespace
448 * This does not mean that all pages in that namespace have the model
450 * @note To determine the default model for a new page's main slot, or any slot in general,
451 * use SlotRoleHandler::getDefaultModel() together with SlotRoleRegistry::getRoleHandler().
453 * @param int $index Index to check
454 * @return null|string Default model name for the given namespace, if set
456 public function getNamespaceContentModel( $index ) {
457 return $this->options
->get( 'NamespaceContentModels' )[$index] ??
null;
461 * Determine which restriction levels it makes sense to use in a namespace,
462 * optionally filtered by a user's rights.
464 * @todo Move this to PermissionManager and remove the dependency here on permissions-related
467 * @param int $index Index to check
468 * @param User|null $user User to check
471 public function getRestrictionLevels( $index, User
$user = null ) {
472 if ( !isset( $this->options
->get( 'NamespaceProtection' )[$index] ) ) {
473 // All levels are valid if there's no namespace restriction.
474 // But still filter by user, if necessary
475 $levels = $this->options
->get( 'RestrictionLevels' );
477 $levels = array_values( array_filter( $levels, function ( $level ) use ( $user ) {
479 if ( $right == 'sysop' ) {
480 $right = 'editprotected'; // BC
482 if ( $right == 'autoconfirmed' ) {
483 $right = 'editsemiprotected'; // BC
485 return ( $right == '' ||
$user->isAllowed( $right ) );
491 // First, get the list of groups that can edit this namespace.
492 $namespaceGroups = [];
493 $combine = 'array_merge';
494 foreach ( (array)$this->options
->get( 'NamespaceProtection' )[$index] as $right ) {
495 if ( $right == 'sysop' ) {
496 $right = 'editprotected'; // BC
498 if ( $right == 'autoconfirmed' ) {
499 $right = 'editsemiprotected'; // BC
501 if ( $right != '' ) {
502 $namespaceGroups = call_user_func( $combine, $namespaceGroups,
503 User
::getGroupsWithPermission( $right ) );
504 $combine = 'array_intersect';
508 // Now, keep only those restriction levels where there is at least one
509 // group that can edit the namespace but would be blocked by the
511 $usableLevels = [ '' ];
512 foreach ( $this->options
->get( 'RestrictionLevels' ) as $level ) {
514 if ( $right == 'sysop' ) {
515 $right = 'editprotected'; // BC
517 if ( $right == 'autoconfirmed' ) {
518 $right = 'editsemiprotected'; // BC
520 if ( $right != '' && ( !$user ||
$user->isAllowed( $right ) ) &&
521 array_diff( $namespaceGroups, User
::getGroupsWithPermission( $right ) )
523 $usableLevels[] = $level;
527 return $usableLevels;
531 * Returns the link type to be used for categories.
533 * This determines which section of a category page titles
534 * in the namespace will appear within.
536 * @param int $index Namespace index
537 * @return string One of 'subcat', 'file', 'page'
539 public function getCategoryLinkType( $index ) {
540 $this->isMethodValidFor( $index, __METHOD__
);
542 if ( $index == NS_CATEGORY
) {
544 } elseif ( $index == NS_FILE
) {